Skip to content

feat(customers): add customer.* webhook events and customer_id list filters - #52

Merged
ericviana merged 5 commits into
mainfrom
eric/customers-webhooks-and-shims
Jul 27, 2026
Merged

feat(customers): add customer.* webhook events and customer_id list filters#52
ericviana merged 5 commits into
mainfrom
eric/customers-webhooks-and-shims

Conversation

@ericviana

@ericviana ericviana commented Jul 27, 2026

Copy link
Copy Markdown
Member

What

Wave 1 of the receivers -> customers migration for blindpay-python: additive,
non-breaking changes that the currently deployed API already accepts.
No renames, no removals, plus a follow-up commit fixing 4 pre-existing
wrong response field names that were already live bugs before this PR.

  • Add customer.new, customer.update, customer.delete to the
    WebhookEvents Literal in resources/webhooks/webhooks.py. The deployed
    WebhookEndpointIn.events enum has included these since June 2026 and
    dual-emit is already active, so subscribing to them works against
    production today.
  • Fold receiver.delete into the same WebhookEvents Literal (it existed
    only as an orphaned, publicly-exported types.WebhookEvent alias,
    disconnected from WebhookEvents and never wired into any webhook
    input/response type). That stray alias is retired.
  • Mark receiver.new / receiver.update / receiver.delete deprecated with
    a comment pointing at the same changelog post (2026-06-04-customers-rename)
    used by the receivers namespace DeprecationWarning in client.py. They
    are not removed since receiver.* webhooks are still dual-emitted by
    the deployed API; removing them now would silently drop deliveries for
    subscribers who haven't migrated.
  • Add customer_id as an optional filter on ListPayinsInput and
    ListPayoutsInput, alongside the existing receiver_id (kept, not
    removed). GET /v1/instances/{id}/payins and /payouts already accept
    customer_id as a query filter in the deployed API spec.
  • Update the README quickstart example to call blindpay.customers.get()
    instead of the runtime-deprecated blindpay.receivers.get().

Also fixes: wrong response field names (pre-existing, live bug)

A prior optimistic rename declared some response field names ahead of the
server. The deployed API still sends receiver_id in these four places, so
the SDK declared a key that never arrives. Each was verified directly
against apps/api/openapi.json (schema name + required-ness) and traced to
the actual response type of its endpoint, not just matched by name:

  • Owner (nested owners on business customers, resources/customers/customers.py):
    declared customer_id, spec ReceiverOut.owners[] uses receiver_id.
  • LimitIncreaseRequest (resources/customers/customers.py): declared
    customer_id, spec GetReceiverLimitIncreaseOut requires receiver_id.
  • OfframpWallet (resources/wallets/offramp.py): declared customer_id,
    spec OfframpWallet requires receiver_id.
  • BlockchainWallet (resources/wallets/blockchain.py): declared
    customer_id, spec BlockchainWalletOut requires receiver_id.

These are TypedDicts, so nothing validated the wrong key at runtime; code
that indexed ["customer_id"] on these objects was already getting a
KeyError today, since the wire never sends that key. Fixing the declared
key doesn't break anything that worked, it aligns the SDK with what
production sends right now, independent of blindpay-v2 PR #1799.

Updated the wallets/blockchain and wallets/offramp test fixtures to use
receiver_id and added an explicit key assertion in one test per file so a
regression of this exact bug fails the suite.

Left untouched, on purpose: receiver_amount (singular) and currency_type
everywhere (different concept, see below); the SDK-level customer_id
method parameters/path args used throughout these same files (correct as
idiomatic Python parameter names, not wire keys); and
bank_accounts.py's OfframpWallet shape, whose deployed BankAccountOut
has neither receiver_id nor customer_id, a separate pre-existing
type-accuracy issue out of scope here.

Why this is safe today

Every change here was verified against the deployed production
openapi.json, not the pending PR spec: the webhook event enum, and the
customer_id query filter on payins/payouts, are already live. Nothing in
this PR depends on blindpay-v2 PR #1799 (still open, not deployed).

Deliberately NOT in this PR (wave 2)

These require blindpay-v2 PR #1799 to ship first, because the deployed API
does not accept them yet. Shipping them now would break every current user
of this SDK:

  • The remaining 11 receiver_* -> customer_* field renames (e.g.
    receiver_local_amount, receiver_wallet_address, receiver_network,
    receiver_token, receiver_invite_redirect_url, receivers_amount ->
    customers_amount, etc). Only a handful of these have any code in this
    SDK to begin with; most of the field list isn't modeled here at all
    (no InstanceOut/single-instance GET).
  • Removing receiver.* from the webhook event enum.
  • Removing the legacy receivers resource (already runtime-deprecated,
    scheduled for v3.0.0 removal per client.py).

Not touched, confirmed unchanged, because these are a different concept
(the amount/side a transaction leg refers to, not the customer resource):
receiver_amount (singular) on quotes/payins/payouts/transfers, and
currency_type/amount_reference still accepting the sender/receiver
string literal values.

Version

Not manually bumped. This repo uses release-please (see
release-please-config.json, .github/workflows/publish.yaml) which bumps
src/blindpay/__init__.py and CHANGELOG.md itself from conventional
commits after merge to main. The original commit uses a feat: prefix and
the follow-up field-name fix commit uses fix:, so release-please will
compute the right version bump on the next release PR, consistent with how
every prior PR in this repo's history has been handled (no prior PR
hand-edits the version or changelog).

Build output

$ uv sync --group dev --group test
Resolved 28 packages, installed 27 packages

$ uv run ruff format --check
73 files already formatted

$ uv run ruff check .
All checks passed!

$ uv run pyright
0 errors, 0 warnings, 0 informations

$ uv run mypy .
Success: no issues found in 72 source files

$ uv run pytest -q
175 passed, 44 warnings in 0.97s

The 44 warnings are all the expected DeprecationWarning from
blindpay.receivers access in test_receivers.py / test_bank_accounts.py,
pre-existing and unrelated to this change.

https://claude.ai/code/session_01F1stiNzuNtJXoXtiW9ZCbs

…ilters

The deployed API already accepts these; this is additive and safe against
production today.

- Add customer.new/customer.update/customer.delete to the WebhookEvents
  Literal, and fold the previously orphaned receiver.delete (from
  src/blindpay/types.py, disconnected from WebhookEvents and never wired
  up) into the same Literal alongside it.
- Mark receiver.new/receiver.update/receiver.delete deprecated with a
  comment pointing at the same changelog post used by the receivers
  namespace deprecation warning in client.py. They are not removed:
  receiver.* webhooks are still dual-emitted by the deployed API.
- Retire the stray, publicly exported types.WebhookEvent alias now that
  receiver.delete lives in WebhookEvents.
- Add customer_id as an optional list filter on ListPayinsInput and
  ListPayoutsInput, alongside the existing receiver_id (not removed).
  GET /v1/instances/{id}/payins and /payouts already accept customer_id
  as a query filter in the deployed API spec.
- Update the README quickstart example to use blindpay.customers.get()
  instead of the runtime-deprecated blindpay.receivers.get().

Deliberately excluded (wave 2, needs blindpay-v2 PR #1799 deployed
first): the remaining 11 receiver_* -> customer_* field renames, and
removal of receiver.* from the webhook enum. Shipping those today would
break every SDK user still on the deployed field names.

Claude-Session: https://claude.ai/code/session_01F1stiNzuNtJXoXtiW9ZCbs
@BernardoSM

BernardoSM commented Jul 27, 2026

Copy link
Copy Markdown
Collaborator

Snyk checks have passed. No issues have been found so far.

Status Scan Engine Critical High Medium Low Total (0)
Code Security 0 0 0 0 0 issues

💻 Catch issues earlier using the plugins for VS Code, JetBrains IDEs, Visual Studio, and Eclipse.

The branch had deleted the public `WebhookEvent` alias from types.py and dropped it
from __init__.py's imports and __all__. It is dead inside the repo, but it is an
exported symbol, so `from blindpay import WebhookEvent` would break on what is
otherwise a purely additive minor release.

Restored with its original value and a deprecation note pointing at WebhookEvents,
which is the alias that actually lists every event. Removal belongs in the next major,
alongside dropping the receiver.* members.

Claude-Session: https://claude.ai/code/session_01F1stiNzuNtJXoXtiW9ZCbs
The customer.* rename shims declared customer_id on nested owners,
limit-increase requests, blockchain wallets, and offramp wallets.
The deployed API still sends receiver_id in all four places (server-side
customers rename is not deployed yet). Since these are TypedDicts, a
consumer indexing the declared key gets a KeyError today.

Verified against apps/api/openapi.json: ReceiverOut.owners[], required
receiver_id on BlockchainWalletOut, OfframpWallet, and
GetReceiverLimitIncreaseOut. Updated the wallet test fixtures to match
and added receiver_id assertions so a regression of this exact bug fails
the suite.

bank_accounts.py's owners field is untouched: the deployed
BankAccountOut has neither receiver_id nor customer_id, so that is a
separate pre-existing type-accuracy issue, not this bug.

Claude-Session: https://claude.ai/code/session_01F1stiNzuNtJXoXtiW9ZCbs
@ericviana

Copy link
Copy Markdown
Member Author

Added a follow-up commit to this branch fixing 4 pre-existing wrong-key bugs found while auditing wave 1 against the deployed openapi.json.

What was wrong

Someone previously renamed some response field names in this SDK ahead of the server. The deployed API still sends receiver_id in these places, so the SDK declared a key that never arrives:

  • Owner (nested owners on business customers, customers.py) declared customer_id. Spec: ReceiverOut.owners[] uses receiver_id.
  • LimitIncreaseRequest (customers.py) declared customer_id. Spec: GetReceiverLimitIncreaseOut requires receiver_id.
  • OfframpWallet (wallets/offramp.py) declared customer_id. Spec: OfframpWallet requires receiver_id.
  • BlockchainWallet (wallets/blockchain.py) declared customer_id. Spec: BlockchainWalletOut requires receiver_id.

Each was confirmed directly against apps/api/openapi.json (schema name + required-ness), and I traced each TypedDict to the actual response type of its endpoint before touching it, not just by name pattern.

Why this is wave-1 safe

These are TypedDicts, so the fix only changes what static type checking (mypy) expects the response shape to be; nothing at runtime was validating the old key. That means:

  • Code that read ["customer_id"] off these objects was already getting a KeyError today, since the wire never sends that key. Nothing that works today stops working.
  • This aligns the SDK with what production sends right now, independent of blindpay-v2 PR #1799.

Updated test_blockchain_wallets.py and test_offramp_wallets.py mocked response fixtures to use receiver_id, and added an explicit assert response["data"][...]["receiver_id"] == ... in one test per file so a regression of this exact bug fails the suite (verified by reverting the fixtures locally and confirming the assertions catch it).

Left untouched, on purpose:

  • receiver_amount (singular) and currency_type (still sender/receiver string values) everywhere, they're a different, unrelated concept.
  • bank_accounts.py's OfframpWallet.offramp_wallets owner-adjacent shape: the deployed BankAccountOut has neither receiver_id nor customer_id, so renaming it would just swap one wrong field for another. That's a separate pre-existing type-accuracy issue, not this bug, and is out of scope here.
  • The SDK-level customer_id method parameters/path args used everywhere in customers.py, wallets/blockchain.py, wallets/offramp.py (e.g. .list(customer_id), CreateOfframpWalletInput.customer_id). Those are the idiomatic Python-facing parameter names for the new customers resource, not wire keys, and are correct as-is.

Build

$ uv sync --group dev --group test
$ uv run ruff format --check .
73 files already formatted
$ uv run ruff check .
All checks passed!
$ uv run mypy .
Success: no issues found in 72 source files
$ uv run pytest -q
175 passed, 44 warnings in 0.97s

No version bump (release-please handles that from conventional commits; used a fix: prefix on this commit).

…ssertions

pyright flagged reportOptionalSubscript on response["data"][0] since
BlindpayApiResponse["data"] is Optional. Add the same is-not-None assert
already used elsewhere in the suite to narrow the type.

Claude-Session: https://claude.ai/code/session_01F1stiNzuNtJXoXtiW9ZCbs
Blockchain wallets, offramp wallets and limit-increase responses go through
addCustomerIdMiddleware, which adds customer_id wherever receiver_id is
present today, and both are required in the post-#1799 spec. Revert the
earlier reversion to receiver_id for those three.

Owner is different: it's a nested owners[] element, which the middleware
does not recurse into, so customer_id is not sent today but will be after
#1799. Declare both keys as NotRequired so neither shape breaks.

Claude-Session: https://claude.ai/code/session_01F1stiNzuNtJXoXtiW9ZCbs
@ericviana
ericviana merged commit 562f219 into main Jul 27, 2026
6 checks passed
@ericviana
ericviana deleted the eric/customers-webhooks-and-shims branch July 27, 2026 16:28
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants